package jnzb.nntp;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Reader;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.List;

import javax.net.SocketFactory;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;

import jnzb.control.Configuration;
import jnzb.control.Controller;
import jnzb.control.events.InfoEventProcessor;
import jnzb.control.util.FileUtils;

import org.apache.commons.net.nntp.NNTPClient;

/**
 * This is a facade to the news provider
 * 
 * @author AA1
 * 
 */
public class NewsProvider {

	private NewsAccount account = null;

	private String currentGroup = null;

	private NNTPClient client;

	// Single download buffer size and creation
	private static final int BUFFER_SIZE = 1024 * 10;
	char by[] = new char[BUFFER_SIZE];

	// 30 second timeout on the client (for reading)
	private static int DEFAULT_TIMEOUT_MS = 30 * 1000;

	private boolean connected = false;

	private InfoEventProcessor infoProcessor;

	private boolean abortSegmentDownload = false;

	/**
	 * @param account
	 *            - all details needed to connect
	 */
	public NewsProvider(NewsAccount account, InfoEventProcessor infoProcessor) {
		this.account = account;
		this.infoProcessor = infoProcessor;
		client = new NNTPClient();

		// Set the default timeout value
		client.setDefaultTimeout(DEFAULT_TIMEOUT_MS);

	}

	/**
	 * Will stop the current download
	 */
	public void abortDownload() {
		abortSegmentDownload = true;
	}

	/**
	 * Downloads an article to a defined filename
	 * 
	 * @param articleID
	 * @param filename
	 * @param controller
	 *            - used to set the group name being used (passes back to the
	 *            UI)
	 * @return k/s. -1 if not downloaded
	 */
	public double getSegment(String articleID, List<String> groups,
			String filename, Controller controller) {

		double speed = -1;

		boolean downloaded = false;

		// Use an interator for the groups in case we need to change it.
		Iterator<String> groupsIterator = groups.iterator();

		while (groupsIterator.hasNext() && !downloaded) {
			String groupToTry = groupsIterator.next();
			controller.getUiDelegate().setCurrentGroup(groupToTry);
			// If we're not already in the group indicated
			if (currentGroup == null || !currentGroup.equals(groups)) {
				try {
					selectNewsgroup(groupToTry, controller);
					// System.out.println("getsegment : group selected="+groupToTry);

					this.currentGroup = groupToTry;
				} catch (IOException e) {
					// TODO - greaceful exit of method
					this.connected = false;
					infoProcessor
							.debug("Error selecting group - connected=false");
				}
			}

			try {
				long startTime = System.currentTimeMillis();
				int bytes = getArticle(articleID, filename, controller);

				if (abortSegmentDownload) {
					// Aborted - set downloaded to true so we exit
					downloaded = true;
					speed = -1;

					// Reset flag
					abortSegmentDownload = false;
				} else {
					double articleTime = System.currentTimeMillis() - startTime;
					downloaded = true;
					// Update the speed
					double k = ((double) bytes) / 1024;
					double timeInS = articleTime / 1000;
					speed = k / timeInS;

					controller.getUiDelegate().setSpeed(speed);
				}

			} catch (IOException e) {
				// Not found in this group, ensure downloaded is false so it
				// tries the next one
				this.connected = false;
				infoProcessor.debug("Error setting article - connected=false");
				speed = -1;
			} catch (ArticleNotFoundException e) {
				// Not found in this group, ensure downloaded is false so it
				// tries the next one
				downloaded = false;
				speed = -1;
				infoProcessor.info("Could not download article");
			}

		}

		// infoProcessor.debug("Downloaded " + filename);
		return speed;
	}

	/**
	 * Wraprs the client command and connects if neccessary
	 * 
	 * @param groupToTry
	 * @param controller
	 * @throws IOException
	 */

	private void selectNewsgroup(String groupToTry, Controller controller)
			throws IOException {
		if (!client.isConnected() || !this.connected) {
			String previousInfo = controller.getUiDelegate().getDisplayedText();
			infoProcessor.info("Connecting and authenticating...");
			connect(true);
			infoProcessor.info(previousInfo);
		}
		client.selectNewsgroup(groupToTry);

	}

	/**
	 * Retrieves the article from the current group and writes it to the
	 * filename
	 * 
	 * @param articleID
	 *            - expects the <>
	 * @param filename
	 *            - absolute destination
	 * @return number of bytes downloaded
	 * @throws IOException
	 * @throws ArticleNotFoundException
	 */
	private int getArticle(String articleID, String filename,
			Controller controller) throws IOException, ArticleNotFoundException {
		String finalArticleID = articleID;
		if (!articleID.startsWith("<")) {
			finalArticleID = "<" + articleID + ">";
		}
		int totalBytes = 0;
		Reader r = null;
		client.setDefaultTimeout(DEFAULT_TIMEOUT_MS);
		r = retrieveArticleBody(finalArticleID, controller);
		client.setDefaultTimeout(DEFAULT_TIMEOUT_MS);
		if (r != null) {
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream(filename);

				boolean keepReading = true;
				int thisRead = 0;
				while (keepReading && !abortSegmentDownload) {
					try {
						thisRead = r.read(by, 0, BUFFER_SIZE);
					} catch (SocketTimeoutException e) {
						infoProcessor.error("Timeout reading socket - ignoring and going around again");
					}
					keepReading = thisRead != -1;
					if (keepReading) {
						totalBytes += thisRead;
						fos.write(toByteArray(by, thisRead), 0, thisRead);
					}
					controller.getUiDelegate().setSegmentProgress(totalBytes);
				}
				fos.close();
				r.close();
			} finally {
				FileUtils.close(fos);
				if (r != null) {
					try {
						r.close();
					} catch (IOException e) {
						// Nothing to do
					}
				}
			}
		} else {
			totalBytes = -1;
			// Reset display
			controller.getUiDelegate().setSegmentProgress(0);

			throw new ArticleNotFoundException();
		}
		// Download was aborted
		if (abortSegmentDownload) {
			// Remove the file
			java.io.File f = new java.io.File(filename);
			f.delete();
			// Set the downloaded amount to 0
			totalBytes = -1;
		}
		// Reset display
		controller.getUiDelegate().setSegmentProgress(0);

		return totalBytes;
	}

	/**
	 * Wrapper around the retrieve method that connects if we're not connected
	 * 
	 * @param articleID
	 * @param controller
	 * @return
	 * @throws IOException
	 */
	public Reader retrieveArticleBody(String articleID, Controller controller)
			throws IOException {
		if (!client.isConnected() || !this.connected) {
			String previousText = controller.getUiDelegate().getDisplayedText();
			infoProcessor.info("Connecting and authenticating...");
			connect(true);
			infoProcessor.info(previousText);
		}
		return client.retrieveArticle(articleID);
	}

	/**
	 * Not quite an accurate description. Takes that char array read from the
	 * NNTPClient class, which contains 8 bit ascii codes in 16 bit primitive
	 * chars! When computing ydec checksums, this difference is important!
	 * 
	 * @param source
	 * @param length
	 * @return the same values, in bytes, not chars.
	 */
	private byte[] toByteArray(char[] source, int length) {
		byte[] byteArray = new byte[length];
		for (int i = 0; i < length; i++) {
			// As the source chars are only 8 bit, we don't lose anything
			byteArray[i] = (byte) source[i];
		}
		return byteArray;
	}

	/**
	 * Connect and authenticate
	 */
	private void connect(boolean force) {
		if (client == null)
			client = new NNTPClient();
		if (!client.isConnected() || force) {
			try {
				currentGroup = null;
				if (account.isSecure()) {
					client.setSocketFactory(SSLSocketFactory.getDefault());
				} else {
					client.setSocketFactory(SocketFactory.getDefault());
				}
				int connectCounter = 0;
				int maxAttempts = Configuration.getInstance()
						.getConnectionAttempts();
				// While we're still inside the limit, or the limit is 0 (try
				// forever)
				while (connectCounter <= Configuration.getInstance()
						.getConnectionAttempts()
						|| maxAttempts == 0) {
					try {
						client.connect(account.getHost(), account.getPort());
						break;
					} catch (ConnectException e) {
						System.out.println(e);
						infoProcessor
								.debug("Error connecting to news provider - attempt "
										+ (++connectCounter)
										+ "/"
										+ maxAttempts);
						if (connectCounter == maxAttempts) {
							throw new NewsConnectException(
									"Error connecting to news account. Gave up after "
											+ maxAttempts + " attempts");
						}
					} catch (SSLHandshakeException e) {
						System.out.println(e);
						infoProcessor
								.debug("Error making secure connection to news provider - attempt "
										+ (++connectCounter)
										+ "/"
										+ maxAttempts);
						if (connectCounter == maxAttempts) {
							throw new NewsConnectException(
									"Error making secure connection to news provider. Gave up after "
											+ maxAttempts + " attempts");
						}
					} catch (Throwable e) {
						throw new NewsConnectException(
								"Severe error connectng to news provider");
					}
				}
				client.authenticate(account.getUsername(), account
						.getPassword());
				this.connected = true;
			} catch (IOException e) {
				// TODO test remove - works with this commented out - not sure
				// why!
				// throw new NewsConnectException(
				// "Cannot authenticate with the news service");
				e.printStackTrace();
			}
		}
	}

	/**
	 * Disconnects from the account. Do before closing down
	 */
	public void disconnect() {
		try {
			currentGroup = null;
			client.disconnect();
			this.connected = false;
		} catch (IOException e) {
			// Nothing left to do here
		}
	}
}
